package what.api.views;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWebBrowser;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.progress.UIJob;

import what.api.Activator;
import what.api.ApiClass;
import what.api.ApiLog;
import what.api.ApiLogDiff;
import what.api.ApiMethod;
import what.api.ApiPackage;
import what.api.ApiSignature;
import what.api.NewApi;

public class WhatApiView extends ViewPart {
	private Text filter;
	private Text doc;
	private TreeViewer viewer;
	private Action action1;
	private Action action2;
	private Action scanAction;
	private Action browseAction;
	
	Image classImage, packageImage, productImage, methodImage, apiAddedImage,
			apiRemovedImage, productDiffImage;

	File log1 = null;
	File log2 = null;

	ApiLog apiLog1 = null, apiLog2 = null;

	ApiLogDiff logDiff = null;
	
	static final String HELP = "How to inspect Java api's in your product\r\n\r\n"+"Create API Product Log: select a folder containing your Java product to create and save a log of all its api's.\r\n"+
	"Load Product API Log: visualize api's in a log generated through 'Create API Product Log'\r\n"+
	"Compare with another: load a second Product API Log to visualize api's added/missing in the log loaded with 'Load Product API'\r\n"+
	"Browse on the web: search information on selected class or method on the web\r\n"+
	"\r\n"+
	"Note. When visualizing api log, you can filter by typing part of the package name, e.g. 'org.eclipse', 'java.lang.Object'. Regexp are not supported.";
	
	class ViewContentProvider implements IStructuredContentProvider,
			ITreeContentProvider {

		public void inputChanged(Viewer v, Object oldInput, Object newInput) {
		}

		public void dispose() {
		}

		public Object[] getElements(Object parent) {
			return getChildren(parent);
		}

		public Object getParent(Object child) {
			return null;
		}

		public Object[] getChildren(Object parent) {
			if (parent == getViewSite()) {
				if (logDiff != null)
					return new Object[] { logDiff };
				if (apiLog1 != null)
					return new Object[] { apiLog1 };
				if (apiLog2 != null)
					return new Object[] { apiLog2 };
				return new Object[0];
			}
			if (parent instanceof ApiLogDiff) {
				ApiLogDiff logDiff = (ApiLogDiff) parent;
				return new Object[] { logDiff.newLog, logDiff.removedLog };
			}
			if (parent instanceof ApiLog) {
				ApiLog log = (ApiLog) parent;
				return log.log.values().toArray();
			}
			if (parent instanceof ApiPackage) {
				ApiPackage apiPackage = (ApiPackage) parent;
				List result = new ArrayList();
				result.addAll(apiPackage.classes.values());
				result.addAll(apiPackage.packages.values());
				return result.toArray();
			}
			if (parent instanceof ApiClass) {
				ApiClass api = (ApiClass) parent;
				return api.methods.values().toArray();
			}
			if (parent instanceof ApiMethod) {
				ApiMethod method = (ApiMethod) parent;
				return method.signatures.values().toArray();
			}
			return new Object[0];
		}

		public boolean hasChildren(Object parent) {
			return parent instanceof ApiLogDiff || parent instanceof File
					|| parent instanceof ApiLog || parent instanceof ApiPackage
					|| parent instanceof ApiClass
					|| parent instanceof ApiMethod;
		}
	}

	class ViewLabelProvider extends StyledCellLabelProvider {

		public void update(ViewerCell cell) {
			Object obj = cell.getElement();
			StyledString styledString = new StyledString();
			if (obj instanceof ApiLogDiff) {
				styledString.append(apiLog1.description);
				styledString.append(" <-> ", StyledString.COUNTER_STYLER);
				styledString.append(apiLog2.description);
			}
			if (obj instanceof ApiLog) {
				ApiLog apiLog = (ApiLog) obj;
				styledString.append(apiLog.description);
				String decorator = String.format(
						"  (%s methods in %s classes)", apiLog.nMethods,
						apiLog.nClasses);
				styledString.append(decorator, StyledString.DECORATIONS_STYLER);
			}
			if (obj instanceof ApiPackage) {
				ApiPackage apiPackage = (ApiPackage) obj;
				styledString.append(apiPackage.packageName);
				String decorator = String.format(
						"  (%s methods in %s classes)", apiPackage.nMethods,
						apiPackage.nClasses);
				styledString.append(decorator, StyledString.DECORATIONS_STYLER);
			}
			if (obj instanceof ApiClass) {
				ApiClass api = (ApiClass) obj;
				styledString.append(api.className);
				String decorator = String
						.format("  (%s methods)", api.nMethods);
				styledString.append(decorator, StyledString.DECORATIONS_STYLER);
			}
			if (obj instanceof ApiMethod) {
				ApiMethod method = (ApiMethod) obj;
				styledString.append(method.name);
			}
			if (obj instanceof ApiSignature) {
				ApiSignature signature = (ApiSignature) obj;
				styledString.append(signature.signature);
			}
			cell.setText(styledString.toString());
			cell.setStyleRanges(styledString.getStyleRanges());
			cell.setImage(getImage(obj));
			super.update(cell);
		}

		public Image getImage(Object obj) {
			if (obj instanceof ApiLogDiff)
				return productDiffImage;
			if (obj instanceof ApiLog) {
				if (logDiff != null && logDiff.newLog == obj)
					return apiAddedImage;
				if (logDiff != null && logDiff.removedLog == obj)
					return apiRemovedImage;
				if (obj == apiLog1)
					return apiAddedImage;
				if (obj == apiLog2)
					return apiRemovedImage;
			}
			if (obj instanceof ApiPackage)
				return packageImage;
			if (obj instanceof ApiClass)
				return classImage;
			if (obj instanceof HashMap)
				return productImage;
			if (obj instanceof ApiMethod)
				return methodImage;
			return null;
		}
	}

	class ApiViewerComparator extends ViewerComparator {
		@Override
		public int compare(Viewer viewer, Object e1, Object e2) {
			if (e1 instanceof ApiLog && e2 instanceof ApiLog) {
				if (e1 == apiLog1)
					return 1;
				else
					return -1;
			}
			if (e1 instanceof ApiPackage && e2 instanceof ApiClass)
				return -1;
			if (e1 instanceof ApiClass && e2 instanceof ApiPackage)
				return 1;
			if (e1 instanceof ApiPackage && e2 instanceof ApiPackage)
				return ((ApiPackage) e1).packageName
						.compareTo(((ApiPackage) e2).packageName);
			if (e1 instanceof ApiPackage && e2 instanceof ApiPackage)
				return ((ApiPackage) e1).packageName
						.compareTo(((ApiPackage) e2).packageName);
			if (e1 instanceof ApiClass && e2 instanceof ApiClass)
				return ((ApiClass) e1).className
						.compareTo(((ApiClass) e2).className);
			if (e1 instanceof ApiMethod && e2 instanceof ApiMethod)
				return ((ApiMethod) e1).name.compareTo(((ApiMethod) e2).name);
			if (e1 instanceof ApiSignature && e2 instanceof ApiSignature)
				return ((ApiSignature) e1).signature
						.compareTo(((ApiSignature) e2).signature);
			return super.compare(viewer, e1, e2);
		}
	}

	/**
	 * Not using regexp at the moment. User can type in package to filter e.g.
	 * com.ibm.team, or package.class, or package.class.method
	 */
	class RegexpViewFilter extends ViewerFilter {

		String regexp;

		RegexpViewFilter(String regexp) {
			this.regexp = regexp;
		}

		@Override
		public boolean select(Viewer viewer, Object parentElement,
				Object element) {
			if (element instanceof ApiLogDiff)
				return true;
			if (element instanceof ApiLog)
				return true;
			String description = getDescription(element);
			if (description.length() >= regexp.length())
				return description.startsWith(regexp);
			return description.startsWith(regexp.substring(0, description
					.length()));
		}

		String getDescription(Object element) {
			if (element instanceof ApiPackage) {
				ApiPackage apiPackage = (ApiPackage) element;
				if (apiPackage.parent == null)
					return apiPackage.packageName;
				return getDescription(apiPackage.parent) + "."
						+ apiPackage.packageName;
			}
			if (element instanceof ApiClass) {
				ApiClass apiClass = (ApiClass) element;
				return getDescription(apiClass.apiPackage) + "."
						+ apiClass.className;
			}
			if (element instanceof ApiMethod) {
				ApiMethod apiMethod = (ApiMethod) element;
				return getDescription(apiMethod.apiClass) + "."
						+ apiMethod.name;
			}
			return "";
		}

	}

	/**
	 * The constructor.
	 */
	public WhatApiView() {
	}

	/**
	 * This is a callback that will allow us to create the viewer and initialize
	 * it.
	 */
	public void createPartControl(Composite parent) {
		classImage = AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/class_obj.gif").createImage();
		packageImage = AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/package_obj.gif").createImage();
		productImage = AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/projects.gif").createImage();
		methodImage = AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/methpub_obj.gif").createImage();
		apiAddedImage = AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/includ_obj.gif").createImage();
		apiRemovedImage = AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/includ_not_obj.gif").createImage();
		productDiffImage = AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/pending_patch.gif").createImage();

		GridLayout gd = new GridLayout();
		gd.marginHeight = 0;
		gd.marginWidth = 0;
		gd.verticalSpacing = 0;
		gd.horizontalSpacing = 0;

		parent.setLayout(gd);
		doc = new Text(parent, SWT.READ_ONLY | SWT.WRAP);
		doc.setBackground(doc.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
		doc.setText(HELP);
		doc.setLayoutData(new GridData(GridData.FILL_BOTH));
		makeActions();
		hookDoubleClickAction();
		contributeToActionBars();
	}

	void switchToViewer() {
		if (filter != null)
			return;
		Composite parent = doc.getParent();
		doc.dispose();
		filter = new Text(parent, SWT.BORDER);
		filter.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
		viewer.getTree().setLayoutData(new GridData(GridData.FILL_BOTH));
		viewer.setContentProvider(new ViewContentProvider());
		viewer.setLabelProvider(new ViewLabelProvider());
		viewer.setComparator(new ApiViewerComparator());
		viewer.setInput(getViewSite());
		hookContextMenu();
		parent.layout();
		final UIJob filterJob = new UIJob("Filtering api's...") {

			@Override
			public IStatus runInUIThread(IProgressMonitor monitor) {
				String text = filter.getText();
				if (text.length() == 0)
					viewer.setFilters(new ViewerFilter[0]);
				else
					viewer
							.setFilters(new ViewerFilter[] { new RegexpViewFilter(
									text) });
				return Status.OK_STATUS;
			}
		};
		filter.addKeyListener(new KeyListener() {

			public void keyReleased(KeyEvent e) {
				filterJob.cancel();
				filterJob.schedule(2000);
			}

			public void keyPressed(KeyEvent e) {
			}
		});
	}
	
	private void hookContextMenu() {
		MenuManager menuMgr = new MenuManager("#PopupMenu");
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager manager) {
				WhatApiView.this.fillContextMenu(manager);
			}
		});
		Menu menu = menuMgr.createContextMenu(viewer.getControl());
		viewer.getControl().setMenu(menu);
		getSite().registerContextMenu(menuMgr, viewer);
	}

	private void contributeToActionBars() {
		IActionBars bars = getViewSite().getActionBars();
		fillLocalPullDown(bars.getMenuManager());
		fillLocalToolBar(bars.getToolBarManager());
	}

	private void fillLocalPullDown(IMenuManager manager) {
		manager.add(browseAction);
	}

	private void fillContextMenu(IMenuManager manager) {
		manager.add(browseAction);
	}

	private void fillLocalToolBar(IToolBarManager manager) {
		manager.add(scanAction);
		manager.add(new Separator());
		manager.add(action1);
		manager.add(new Separator());
		manager.add(action2);
		manager.add(new Separator());
		manager.add(browseAction);
	}

	String getSearchDescription(ApiClass apiClass) {
		String result = "";
		ApiPackage apiPackage = apiClass.apiPackage;
		while (apiPackage != null) {
			result = apiPackage.packageName + "." + result;
			apiPackage = apiPackage.parent;
		}
		result += apiClass.className;
		return result;
	}

	private void makeActions() {
		browseAction = new Action() {
			public void run() {
				try {
					Object selection = ((IStructuredSelection) viewer
							.getSelection()).getFirstElement();
					String string = null;
					if (selection instanceof ApiClass) {
						ApiClass apiClass = (ApiClass) selection;
						string = getSearchDescription(apiClass);
					}
					if (selection instanceof ApiMethod) {
						ApiMethod apiMethod = (ApiMethod) selection;
						string = getSearchDescription(apiMethod.apiClass) + " "
								+ apiMethod.name;
					}
					if (string == null) {
						MessageBox mb = new MessageBox(getSite().getShell(),
								SWT.ICON_INFORMATION);
						mb
								.setMessage("Select a class or a method below to search it on the web");
					}
					IWebBrowser webBrowser = PlatformUI.getWorkbench()
							.getBrowserSupport().createBrowser("what.api");
					webBrowser.openURL(new URL(String.format(
							"http://www.google.com/search?q=%s", string)));
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		};
		browseAction.setText("Browse");
		browseAction.setToolTipText("Browse on the web");
		browseAction.setImageDescriptor(AbstractUIPlugin
				.imageDescriptorFromPlugin(Activator.PLUGIN_ID,
						"icons/idea_obj.gif"));
		action1 = new Action() {
			public void run() {
				FileDialog dialog = new FileDialog(getSite().getShell());
				String result = dialog.open();
				if (result == null)
					return;

				log1 = new File(result);
				Job job = new Job("Loading product api " + result) {
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						try {
							monitor.beginTask("Loading product api", IProgressMonitor.UNKNOWN);
							apiLog1 = ApiLog.create(log1);
							if (apiLog2 != null) {
								logDiff = new ApiLogDiff(apiLog1, apiLog2);
								logDiff.resolve();
							}
							getSite().getShell().getDisplay().asyncExec(
									new Runnable() {
										public void run() {
											switchToViewer();
											viewer.refresh();
										}
									});
						} catch (Exception e) {
							return new Status(Status.ERROR, "WhatApi",
									"Can't load product api log", e);
						}
						return Status.OK_STATUS;
					}
				};
				job.setUser(true);
				job.schedule();
			}
		};
		action1.setText("Load Product Api");
		action1
				.setToolTipText("Load a Product Api log generated with 'Create Product Api'");
		action1.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/includ_obj.gif"));

		action2 = new Action() {
			public void run() {
				FileDialog dialog = new FileDialog(getSite().getShell());
				final String result = dialog.open();
				if (result == null)
					return;

				Job job = new Job("Loading product api " + result) {
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						monitor.beginTask("Loading product api", IProgressMonitor.UNKNOWN);
						log2 = new File(result);
						try {
							apiLog2 = ApiLog.create(log2);
							if (apiLog1 != null) {
								logDiff = new ApiLogDiff(apiLog1, apiLog2);
								logDiff.resolve();
							}
							getSite().getShell().getDisplay().asyncExec(
									new Runnable() {
										public void run() {
											switchToViewer();
											viewer.refresh();
										}
									});

						} catch (Exception e) {
							return new Status(Status.ERROR, "WhatApi",
									"Can't load product api log", e);
						}
						return Status.OK_STATUS;
					}
				};
				job.setUser(true);
				job.schedule();
			}
		};
		action2.setText("Compare with other Product Api");
		action2
				.setToolTipText("Compare with another Product Api log to see new api's");
		action2.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin(
				Activator.PLUGIN_ID, "icons/includ_not_obj.gif"));

		scanAction = new Action() {
			public void run() {

				DirectoryDialog dialog = new DirectoryDialog(getSite()
						.getShell());
				dialog.setText("Pick Product Location");
				final String productLocation = dialog.open();
				if (productLocation == null)
					return;
				FileDialog fileDialog = new FileDialog(getSite().getShell());
				fileDialog.setText("Select where to save Product API Log");
				final String result = fileDialog.open();
				if (result == null)
					return;

				Job job = new Job("Generating product api log for " + result) {
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						monitor.beginTask("Generating product api log", IProgressMonitor.UNKNOWN);
						try {
							BufferedWriter bw = new BufferedWriter(
									new FileWriter(result));
							NewApi.parse(new File(productLocation), bw);
							bw.close();
						} catch (IOException e) {
							return new Status(Status.ERROR, "WhatApi",
									"Can't generate product api log", e);
						}
						return Status.OK_STATUS;
					}
				};
				job.setUser(true);
				job.schedule();
			}
		};
		scanAction.setImageDescriptor(AbstractUIPlugin
				.imageDescriptorFromPlugin(Activator.PLUGIN_ID,
						"icons/add_obj.gif"));
		scanAction.setText("Generate Product Api");
		scanAction
				.setToolTipText("Create a Product Api log by scanning all jars and classes in a given folder");

	}

	private void hookDoubleClickAction() {
	}

	private void showMessage(String message) {
		MessageDialog.openInformation(viewer.getControl().getShell(),
				"Whatapi", message);
	}

	/**
	 * Passing the focus request to the viewer's control.
	 */
	public void setFocus() {
		if (viewer != null)
			viewer.getControl().setFocus();
	}
}